Перейти к основному содержимому

5.15. Работа с памятью и сборка мусора

Разработчику Архитектору

Работа с памятью и сборка мусора

Работа с памятью и сборка мусора
Автоматическая сборка мусора (GC) на основе mark-and-sweep.
Слабые таблицы (__mode = "k" или "v") — контроль за ссылками.
Профилирование: collectgarbage("count"), принудительный вызов GC.
Нет указателей, но есть ссылки на таблицы и функции.

Lua — язык с автоматическим управлением памятью. Программист не отвечает за выделение и освобождение объектов вручную, как в C или C++. Вместо этого Lua использует сборщик мусора (Garbage Collector, GC), который автоматически определяет и освобождает память, занятую объектами, более недоступными для программы.

Этот механизм позволяет сосредоточиться на логике приложения, но требует понимания его принципов: ведь даже автоматическая система может привести к утечкам, паузам или непредсказуемому поведению, если использоваться без учёта её особенностей.

Lua использует гибридную модель управления памятью, сочетающую стек и кучу (heap).

  1. Стек вызовов (call stack) хранит локальные переменные, параметры функций и информацию о возврате, управляется интерпретатором строго по принципу LIFO. Объекты на стеке существуют только во время выполнения соответствующей функции. При выходе из функции локальные ссылки удаляются — но сами объекты (если они находятся в куче) могут сохраняться. Важно: стек хранит значения примитивных типов (числа, булевы, nil) и указатели на объекты в куче (таблицы, функции, строки).
  2. Куча (heap), централизованная область памяти, где размещаются все составные объекты: таблицы, функции (включая замыкания), потоки (coroutines), строки (неизменяемые, часто кэшируются). Управление памятью в куче полностью делегировано сборщику мусора. Все объекты в куче управляются по ссылкам: переменные содержат не сами объекты, а ссылки на них.
local a = { x = 1 }
local b = a -- b ссылается на тот же объект в куче
b.x = 2
print(a.x) -- 2: изменение через одну ссылку видно через другую

Таким образом, Lua реализует ссылочную семантику для составных типов, аналогично Python, Java или JavaScript.

Жизненный цикл объекта в куче включает несколько этапов:

  • Выделение — при создании ({}, function(), coroutine.create()).
  • Использование — объект доступен через одну или несколько ссылок.
  • Недоступность — все прямые и косвенные ссылки на объект утеряны.
  • Сборка мусора — GC помечает объект как "мертвый" и освобождает память.
  • Финализация (опционально) — если у объекта есть метод __gc, он вызывается перед освобождением.

Сборщик мусора работает невидимо для большинства программ, но его поведение можно анализировать, настраивать и даже принудительно запускать.

Lua использует incremental mark-and-sweep garbage collector — это означает, что процесс сборки разбит на этапы и выполняется по частям между шагами исполнения программы.

GC работает в два этапа:

  1. Mark (пометка). GC начинает с корней — глобальных переменных, локальных переменных в активных стеках, регистров VM, рекурсивно помечает все объекты, достижимые по ссылкам. Каждый достижимый объект получает флаг «жив».
  2. Sweep (очистка). Проход по всем объектам в куче, непомеченные объекты (недостижимые) освобождаются, а память возвращается системе или пулу. Для объектов с метаметодом __gc вызывается финализатор на следующем цикле GC, чтобы избежать проблем с порядком удаления.

Этапы mark и sweep выполняются порциями, чтобы минимизировать паузы. Это критично для реального времени (например, игры), и называется инкрементальность.

GC запускается автоматически, когда объём выделённой памяти превышает порог, рассчитываемый на основе предыдущего объёма живых объектов и коэффициентов сборки.

Нужно ли вызывать GC вручную? Функция collectgarbage() позволяет взаимодействовать с GC явно:

collectgarbage("collect")      -- Принудительный запуск полного цикла GC
collectgarbage("count") -- Возвращает текущий объём памяти в КБ
collectgarbage("step", step) -- Выполнить один шаг сборки
collectgarbage("stop") -- Остановить GC
collectgarbage("restart") -- Возобновить GC

Когда стоит использовать принудительный вызов?

  • После загрузки ресурсов (например, уровня в игре), чтобы очистить временные данные.
  • Перед критическими участками (анимация, ввод), чтобы минимизировать паузы GC.
  • Для профилирования — отслеживание роста памяти.

И соответственно, не нужно в обычном потоке выполнения (GC и так работает эффективно), а частые вызовы collectgarbage("collect") могут ухудшить производительность, так как нарушают адаптивность GC. Используйте ручной GC целенаправленно и редко, как инструмент управления пиковыми нагрузками.

Все составные типы в Lua передаются и хранятся по ссылке.

Это означает, что:

  1. Присваивание таблицы не создаёт копию:
local a = { x = 1 }
local b = a

Теперь a и b указывают на один объект. Удаление a не освободит память, пока b существует.

  1. Замыкания захватывают переменные по ссылке:
function make_counter()
local count = 0
return function() count = count + 1; return count end
end

Объект count будет жить до тех пор, пока живёт замыкание. Таким образом, главная причина утечек памяти в Lua — непреднамеренное удержание ссылок.

Lua предоставляет механизм слабых ссылок через слабые таблицы, создаваемые с помощью метаметода __mode.

local weak_k = {}
setmetatable(weak_k, { __mode = "k" }) -- слабые ключи

local weak_v = {}
setmetatable(weak_v, { __mode = "v" }) -- слабые значения

local weak_kv = {}
setmetatable(weak_kv, { __mode = "kv" }) -- слабые ключи и значения

Как работают слабые ссылки? Слабая ссылка не предотвращает сборку мусора объекта. Если объект достигается только через слабые ссылки, он считается недостижимым и будет собран.

Пример - кэш объектов:

local cache = setmetatable({}, { __mode = "v" })

function get_or_create(key)
if not cache[key] then
cache[key] = expensive_creation(key)
end
return cache[key]
end

Здесь значения (объекты) будут автоматически удаляться GC, если на них нет других ссылок. Это позволяет строить автоматически очищающиеся кэши.

Слабые ссылки работают только для таблиц и userdata. Строки и числа не участвуют в GC (строки — иммутируемые, часто interned).

Объекты типа userdata (и таблицы, если включено __gc) могут иметь финализатор — код, выполняемый перед освобождением.

local obj = newproxy(true)  -- userdata-like object
getmetatable(obj).__gc = function(self)
print("Объект уничтожается")
end

Финализаторы вызываются не сразу после потери доступности, а на следующем цикле GC. Не стоит полагаться на них для освобождения внешних ресурсов (файлы, сокеты) — лучше делать это явно. Финализаторы могут создавать новые ссылки на объект — тогда он воскрешается (resurrection) и не будет удалён.

Lua предоставляет простые, но эффективные средства для мониторинга использования памяти.

  1. collectgarbage("count") возвращает текущий объём используемой памяти в килобайтах (с плавающей точкой):
print(collectgarbage("count"))  -- например, 123.456

Можно использовать для измерения потребления памяти до/после операции и для поиска утечек: если значение постоянно растёт без плато.

  1. Дополнительные метрики:
-- Общий объём памяти (в КБ)
collectgarbage("count")

-- Количество вызовов GC
collectgarbage("stat") -- в некоторых реализациях (например, LuaJIT)

-- Настройка поведения GC
collectgarbage("setpause", 110) -- % использования до следующего цикла
collectgarbage("setstepmul", 200) -- скорость сборки относительно аллокации
  1. Внешние инструменты:
    • luatrace — трассировка выделений.
    • glue.gccount (в наборах расширений) — детальный подсчёт.
    • Интеграция с debug-версиями движков (например, в Love2D или Nginx + OpenResty).